/* This xpcom holds informations about every visited page.
Infos are:
1) [key] pageID of the visited page (random number, URL crc32 hash or Content crc32 hash)
2) [value[0]] URL of the visited page
3) [value[1]] time spent
4) [value[2]] Has the page been printed?
5) [value[3]] Has the page been bookmarked?
6) [value[4]] Timestamps array, containing Date objects, pushed everytime the page is focused
7) [value[5]] Cluster ID
*/

/***********************************************************
constants
***********************************************************/

// reference to the interface defined in pcIPageTimer.idl
const pcIPageTimer = Components.interfaces.pcIPageTimer;

// reference to the required base interface that all components must support
const nsISupports = Components.interfaces.nsISupports;

// UUID uniquely identifying our component
// You can get from: http://kruithof.xs4all.nl/uuid/uuidgen here
const CLASS_ID = Components.ID("{69a2a400-38ee-11db-a98b-0800200c9a66}");

// description
const CLASS_NAME = "This xpcom takes care of the time spent on every page";

// textual unique identifier
const CONTRACT_ID = "@peercafe.org/pcPageTimer;1";

/***********************************************************
class definition
***********************************************************/

//class constructor
function pcPageTimer() {
};

// class definition
pcPageTimer.prototype = {
  // name of the file that represents the serialized object contained in this class
  serializedFileName: 'pcPageTimer.ser',

  timeHashTable: {}, // "hash table" declaration
  // key = pageID
  // value = page Info => Array[6]
  // 0 = URL
  // 1 = time spent
  // 2 = printed?
  // 3 = bookmarked?,
  // 4 = array of timestamps
  // 5 = ClusterID

  /* info reguarding the last page on focus:
  	 lastFocusInfo['pageID'] is the ID of the latest focused page so that when there is a switch in the focus
     we know which tab's timing has to be updated.
     lastFocusInfo['focusTime'] is the instant in which the tab was focused */
  lastFocusInfo: {},
	
  maxTime: 900, // max seconds spent on a page

  /* A new webpage, second spent on that page = 0, otherwise handle the time spent from the focus of the current
  tab untill now */
  handleFocus: function(pageID, pageURL, pageContent){
	var logger = Components.classes['@peercafe.org/pcLogger;1']
					.createInstance(Components.interfaces.pcILogger);
	try{
		/* set the time spent on the PREVIOUS focused page,
		check if there is a previous tab in focus or if it's the first one of the session */
		this.handleLastFocus()
		
		// set as focused the current page
		this.lastFocusInfo['pageID'] = pageID
		// set as focus-time the current time
		now = new Date()
		this.lastFocusInfo['focusTime'] = now.getTime()
		
		if (pageID in this.timeHashTable){
			// timestamp ++, adding the current timestamp to the array of timestamps
			
			// last recorded date
			var lastDate = this.timeHashTable[pageID][4][this.timeHashTable[pageID][4].length - 1]
			var now = new Date()
			
			// at least one second between every real switch
			if (now - lastDate > 1000){
				this.timeHashTable[pageID][4].push(now)
				/*logger.pcLog('added a timestamp to: ' + pageURL + ' -> ' + now)*/
			}
			
		} else {
			// initialization, 0 seconds spent there.
	        logger.pcLog('visited a new page: ' + pageURL)
			
			var timeStampArray = new Array()
			var now = new Date()
			timeStampArray.push(now) // first timestamp
			logger.pcLog('added the first timestamp to: ' + pageURL + ' -> ' + now)
			
			var pageInfo = new Array()
			pageInfo[0] = pageURL // URL
			pageInfo[1] = 0 // time
			pageInfo[2] = false // printed?
			pageInfo[3] = false // bookmarked?
			pageInfo[4] = timeStampArray // array of timestamps, every timestamp is a visit (focus)
			
			var size = 0
			for (x in this.timeHashTable){
				size++
			}
			
			pageInfo[5] = size // unique id (STRING), the size of the hashmap is fine for that
			
			this.timeHashTable[pageID] = pageInfo
			
			//logger.pcLog("PAGE CONTENT in handleFocus: " + pageContent + "\n")
			// now cluster the content of the page
			/*
		    const cid = "@peercafe.org/XPCOMSample/PeerCafe;1";
		    obj = Components.classes[cid].createInstance();
		    obj = obj.QueryInterface(Components.interfaces.IPeerCafe);
		   	// calling the xpcom Cluster page
			obj.Cluster(pageInfo[5] + "", pageContent);
			*/
			
		}
		
	} catch(e){
		logger.pcLog('Error into createEntry ' + e)
	}
  },

  /* set the time for the previous focused page */
  handleLastFocus: function(){
	try{
		if (this.lastFocusInfo['pageID'] != undefined){
			now = new Date()
			var timeSpent = Math.round(((now.getTime() - this.lastFocusInfo['focusTime']) / 1000))
			
			// TODO pref?
			if (timeSpent > this.maxTime){
				timeSpent = this.maxTime // max time spent on a page = 15 mins
			}
			
			this.timeHashTable[this.lastFocusInfo['pageID']][1] += timeSpent
			
			this.lastFocusInfo['pageID'] = undefined // since we are closing the window
			// nb: on macos the process will stay alive, that's why we need to set it to undefined
		}
	} catch (e){
		return 'Error into handleLastFocus ' + e;
	}
  },

  // return a string representing the content of the hashtable
  getTableContent: function(){
	try{
		var stringToPrint = ""
		for (x in this.timeHashTable) {
			stringToPrint += 'ID: ' + x + ', seconds: ' + this.timeHashTable[x][1] + '\n';
		}
		return 'List: ' + stringToPrint
	}catch(e){
		return 'Error into getTableContent ' + e;
	}
  },

  // return a string representation for the whole page information readable by a tree iterator
  infoToString: function(){
	try{
		var infoString = ""
		for (x in this.timeHashTable) {
			infoString += x + '.ITEM.'
			infoString += this.timeHashTable[x][0] + '.ITEM.' // url
			infoString += this.timeHashTable[x][1] + '.ITEM.' // time
			infoString += this.timeHashTable[x][2] + '.ITEM.' // printed?
			infoString += this.timeHashTable[x][3] + '.ITEM.' // bookmarked?
			
			for (y in this.timeHashTable[x][4]){ // timestamps
				infoString += this.timeHashTable[x][4][y].getTime() + '.TIMEITEM.'
			}
			
			infoString += '.ITEM.'
			
			infoString += this.timeHashTable[x][5] + '.NEXT.' // idCluster
		}
		return infoString
	}catch(ex){
		return 'Error into infoToString ' + e;
	}
  },

  /* implementation dependent!!!!
  	 this method serializes a string representation of the whole object to disk */
  serialize: function(){
	var logger = Components.classes['@peercafe.org/pcLogger;1']
					.createInstance(Components.interfaces.pcILogger);
	try{
		var content = this.infoToString() // stuff to write to file
		
		/* TODO USE ME, later on.. the problem now is that path has to be
		native, like c:\\peercafe\\ and so on.. neither ~/peercafe/ is a valid one
		
		var prefManager = Components.classes["@mozilla.org/preferences-service;1"]
							.getService(Components.interfaces.nsIPrefBranch);
		var folder = prefManager.getCharPref("extensions.peercafe.loggingFolder"); // in which folder??
		*/

		// file handler
		var file = Components.classes["@mozilla.org/file/directory_service;1"]
		                     .getService(Components.interfaces.nsIProperties)
		                     .get('Home', Components.interfaces.nsIFile);
		
		file.append(this.serializedFileName)
		
		// file is nsIFile, data is a string
		var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
		                         .createInstance(Components.interfaces.nsIFileOutputStream);

		foStream.init(file, 0x02 | 0x08, 0664, 0); // write, create
		foStream.write(content, content.length);
		foStream.close();
		logger.pcLog('pcPageTimer@serialize: task suffesfully accomplished')
	}catch(e){
        logger.pcLog('Exception in xpcom pcPageTimer@serialize: ' + e)
	}
  },

  // returns true if the given string is 'true', false otherwise (a sort of parseInt())
  parseBool: function(boolString){
	return boolString == 'true';
  },

  /* this method deserializes the string representation fot his object and loads it
     as soon as firefox starts */
  deserialize: function(){
	var logger = Components.classes['@peercafe.org/pcLogger;1']
					.createInstance(Components.interfaces.pcILogger);
	try{
		// get the file
		var data = "";
		var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]
		                        .createInstance(Components.interfaces.nsIFileInputStream);
		var sstream = Components.classes["@mozilla.org/scriptableinputstream;1"]
		                        .createInstance(Components.interfaces.nsIScriptableInputStream);
		
		// file handler
		var file = Components.classes["@mozilla.org/file/directory_service;1"]
			                     .getService(Components.interfaces.nsIProperties)
			                     .get('Home', Components.interfaces.nsIFile);

		file.append(this.serializedFileName)
		
		if (file.exists()){
			fstream.init(file, -1, 0, 0);
			sstream.init(fstream); 

			// read the content
			var str = sstream.read(4096);
			while (str.length > 0) {
			  data += str;
			  str = sstream.read(4096);
			}

			sstream.close();
			fstream.close();

			//logger.pcLog('read from the serialized file: ' + data)

			// inject the content into the local structure
			var pages = data.split(".NEXT.");

			for (var i = 0; i < pages.length - 1; i++) {
				var pieces = pages[i].split(".ITEM.");

				var pageID =  pieces[0] // pageID
				var pageInfo = new Array()
				pageInfo[0] = pieces[1] // URL
				pageInfo[1] = parseInt(pieces[2]) // time
				pageInfo[2] = this.parseBool(pieces[3]) // printed?
				pageInfo[3] = this.parseBool(pieces[4]) // bookmarked?
				
				var timeStampsArray = pieces[5] // timestamps
				
				var timeStamps = timeStampsArray.split(".TIMEITEM.") // timestamps = length - 1
				
				pageInfo[4] = new Array() // timestamp array initialization
				
				for (var z = 0; z < timeStamps.length - 1; z++){
					pageInfo[4].push(new Date(parseInt(timeStamps[z])))
				}
				
				pageInfo[5] = pieces[6] // cluster ID (string (int))
				
				this.timeHashTable[pageID] = pageInfo
		    }
			logger.pcLog('pcPageTimer@deserialize: task suffesfully accomplished')
		} else {
			logger.pcLog('pcPageTimer@deserialize: ' + this.serializedFileName +
			 	' not found, first execution?')
		}
	}catch(e){
		logger.pcLog('Exception in xpcom pcPageTimer@deserialize: ' + e)
	}
  },

  trend: function(){
	var logger = Components.classes['@peercafe.org/pcLogger;1']
					.createInstance(Components.interfaces.pcILogger);
	try{
		var alpha = 0.8
		var beta = 0.5
		var result = ""
		for (x in this.timeHashTable){
			var timeStamps = this.timeHashTable[x][4] // array of timestamps
			var slow = this._slowFast(beta, timeStamps)
			var fast = this._slowFast(alpha, timeStamps)
			var trend = slow - fast
			
			if(trend > 0){
				result = "+"
			} else {
				result = "-"
			}
			
			logger.pcLog(result + '(' + trend + ') -> ' + this.timeHashTable[x][0])
		}
		
	}catch(e){
		logger.pcLog('Exception in xpcom pcPageTimer@trend: ' + e)
	}
  },

  _slowFast: function(alphaBeta, timeStampArray){
	var logger = Components.classes['@peercafe.org/pcLogger;1']
					.createInstance(Components.interfaces.pcILogger);
	try{
		var last = new Date()
		var delta = 0.0;

		for(i = 0; i < timeStampArray.length - 1; i++){
			delta = alphaBeta * delta + (1 - alphaBeta) * (timeStampArray[i+1] - timeStampArray[i]);
		}
		delta = alphaBeta * delta + (1 - alphaBeta) * (last - timeStampArray[timeStampArray.length - 1]);
		return Math.round(delta)
	}catch(e){
			logger.pcLog('Exception in xpcom pcPageTimer@_slowFast: ' + e)
	}
	
  },

  /* Given an array of timestamps, it returns an array of occurrences in days.
  Every element of the array is a day and its content is the number of visits that
  the page had that day. The Array goes from the first visit to today.
  Days with no visits contain 0  */
  fromStampsToDays: function(timeStamps){
	var frequencyDays = new Array() // array of days
	var currentDayIndex = 0; // day index
	frequencyDays[currentDayIndex] = 1; // first timestamp (into the first day)
	
	// fill the frequencyDays with the days that we have
	for (var i = 0; i < timeStamps.length - 1; i++){
		if(this._isSameDay(timeStamps[i], timeStamps[i+1])){
			frequencyDays[currentDayIndex]++
		} else {
			var daysBetween = this._fillWith0(timeStamps[i], timeStamps[i+1]) // 0 between the 2 days
			currentDayIndex += daysBetween + 1 // the next day is X days after the previous
			frequencyDays[currentDayIndex] = 1; // new day, first visit
		}
	}
	// now put 0 untill the date of today	
	this._fillWith0(frequencyDays,timeStamps[timeStamps.length - 1], new Date())
	return frequencyDays;
  },

  /* Returns true if the first timestamp belongs to the same day, month and year of
  the second timestamp, false otherwise */
  _isSameDay: function(firstStamp, secondStamp){
	return (
		(firstStamp.getDate() == firstStamp.getDate()) &&
		(firstStamp.getMonth() == firstStamp.getMonth()) &&
		(firstStamp.getYear() == firstStamp.getYear()));
  },

  /* Given the array of days , a first and a second date, this function will 
  compute how many days are between the 2 dates and fill the array of days with 0
  (visits) for every day between the 2 dates
  The function returns the number of days between the 2 dates */
  _fillWith0: function(arrayToFill, firstDate, secondDate){
	var daysBetween = 0.0
	daysBetween = Math.floor((secondDate - firstDate) / 86400000)
	
	for (var i = 0; i < daysBetween; i++){
		arrayToFill[arrayToFill.length - 1] = 0 // last == 0
	}
	
	return daysBetween;
  },

  QueryInterface: function(aIID){
    if (!aIID.equals(pcIPageTimer) &&    
        !aIID.equals(nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

/***********************************************************
class factory

This object is a member of the global-scope Components.classes.
It is keyed off of the contract ID. Eg:

mypcPageTimer= Components.classes["@dietrich.ganx4.com/pcPageTimer;1"].
                          createInstance(Components.interfaces.pcIPageTimer);

***********************************************************/
var pcPageTimerFactory = {
  createInstance: function (aOuter, aIID)
  {
    if (aOuter != null)
      throw Components.results.NS_ERROR_NO_AGGREGATION;
    return (new pcPageTimer()).QueryInterface(aIID);
  }
};

/***********************************************************
module definition (xpcom registration)
***********************************************************/
var pcPageTimerModule = {
  _firstTime: true,
  registerSelf: function(aCompMgr, aFileSpec, aLocation, aType)
  {
    aCompMgr = aCompMgr.
        QueryInterface(Components.interfaces.nsIComponentRegistrar);
    aCompMgr.registerFactoryLocation(CLASS_ID, CLASS_NAME, 
        CONTRACT_ID, aFileSpec, aLocation, aType);
  },

  unregisterSelf: function(aCompMgr, aLocation, aType)
  {
    aCompMgr = aCompMgr.
        QueryInterface(Components.interfaces.nsIComponentRegistrar);
    aCompMgr.unregisterFactoryLocation(CLASS_ID, aLocation);        
  },
  
  getClassObject: function(aCompMgr, aCID, aIID)
  {
    if (!aIID.equals(Components.interfaces.nsIFactory))
      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;

    if (aCID.equals(CLASS_ID))
      return pcPageTimerFactory;

    throw Components.results.NS_ERROR_NO_INTERFACE;
  },

  canUnload: function(aCompMgr) { return true; }
};

/***********************************************************
module initialization

When the application registers the component, this function
is called.
***********************************************************/
function NSGetModule(aCompMgr, aFileSpec) { return pcPageTimerModule; }
